package neural;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Scanner;

public class Net {

	private static Net instance;
	ArrayList<double[][]> weights;
	// minInit: valor minimo de inicializacion de los pesos
	double minInit = -2;
	// maxInit: valor minimo de inicializacion de los pesos
	double maxInit = 2;
	int[] layers;
	double[] expectedOutput;
	double[][] dataInput;
	Function function;
	Function function_d;
	double eta = 0.1;
	double minR = -1;
	double maxR = 1;

	public static Net getInstance() {
		if (instance == null) {
			instance = new Net();
		}
		return instance;
	}

	public void initialize(int entradas, int[] nodos, Function f) {
		int len = nodos.length;
		layers = new int[nodos.length + 2];
		layers[0] = entradas;
		for (int i = 0; i < len; i++) {
			layers[i + 1] = nodos[i];
		}
		layers[nodos.length + 1] = 1;
		try {
			datos("samples4.txt");
		} catch (FileNotFoundException e) {
			System.out.println("No se encontro el archivo de datos");
			e.printStackTrace();
		}
		cambiarRango();
		function = f;
		function_d = new TanghDerivada(f.beta);
	}

	public Net() {

	}

	public List<Double> newNet() {
		weights = new ArrayList<double[][]>();
		for (int i = 0; i <= layers.length - 2; i++) {
			weights.add(random(layers[i + 1], layers[i] + 1));
		}
		return toList();
	}

	public double getMinInit() {
		return minInit;
	}

	public double getMaxInit() {
		return maxInit;
	}

	public int[] getLayers() {
		return layers;
	}

	public double[] getExpectedOutput() {
		return expectedOutput;
	}

	public double[][] getDataInput() {
		return dataInput;
	}

	public Function getFunction() {
		return function;
	}

	private List<Double> toList() {
		List<Double> resp = new ArrayList<Double>();
		for (double[][] w : weights) {
			for (int i = 0; i < w.length; i++) {
				for (int j = 0; j < w[i].length; j++) {
					resp.add(w[i][j]);
				}
			}
		}
		return resp;
	}

	private void fromList(List<Double> pesos) {
		weights = new ArrayList<double[][]>();
		Iterator<Double> it = pesos.iterator();
		for (int i = 0; i < layers.length - 1; i++) {
			weights.add(new double[layers[i + 1]][layers[i] + 1]);
			for (int j = 0; j < layers[i + 1]; j++) {
				for (int k = 0; k < layers[i] + 1; k++) {
					weights.get(i)[j][k] = it.next();
				}
			}
		}
	}

	public int listSize() {
		int count = 0;
		for (int i = 0; i <= layers.length - 2; i++) {
			for (int j = 0; j < layers[i + 1]; j++) {
				for (int k = 0; k < layers[i] + 1; k++) {
					count++;
				}
			}
		}
		return count;
	}

	private double[][] random(int rows, int cols) {
		double[][] pesos = new double[rows][cols];
		double aux;
		Random r = new Random();
		for (int i = 0; i < rows; i++) {
			for (int j = 0; j < cols; j++) {
				aux = r.nextDouble();
				aux = (aux - 0.5) * ((maxInit - minInit) / 1)
						+ ((maxInit + minInit) / 2);
				pesos[i][j] = aux;
			}
		}
		return pesos;
	}

	public String toString(List<Double> pesos) {
		List<Double> resp = new ArrayList<Double>();
		for (double[][] w : weights) {
			for (int i = 0; i < w.length; i++) {
				for (int j = 0; j < w[i].length; j++) {
					resp.add(w[i][j]);
				}
			}
		}
		String s = "";
		for (int i = 0; i < weights.size(); i++) {
			s = s.concat("[");
			for (int j = 0; j < weights.get(i).length; j++) {
				for (int k = 0; k < weights.get(i)[j].length; k++) {
					s = s.concat(String.valueOf(weights.get(i)[j][k]) + ", ");
				}
				s = s.concat("\n");
			}
			s = s.concat("]\n");
		}
		return "Multicapa [pesos=" + s + "]";
	}

	public double error(List<Double> pesos) {
		fromList(pesos);

		return error();
	}

	private double error() {

		double[] netOutput = evaluate(dataInput);
		if (expectedOutput.length != netOutput.length) {
			new Error(
					"distinta longitud entre las salidas esperadas y las obtenidas");
		}
		double sum = 0;
		for (int i = 0; i < netOutput.length; i++) {
			sum = sum + Math.pow(expectedOutput[i] - netOutput[i], 2);
		}
		sum = sum / 2;
		return sum / dataInput.length;
		// return sum;
	}

	public double[] evaluate(double[][] input) {
		double[] resp = new double[input.length];
		for (int i = 0; i < input.length; i++) {
			ArrayList<double[]> salidas = new ArrayList<double[]>(layers.length);
			salidas.add(input[i]);
			for (int j = 0; j < weights.size(); j++) {
				double[] aux = new double[weights.get(j).length + 1];
				aux[0] = -1;
				for (int k = 0; k < weights.get(j).length; k++) {
					double sum = 0;
					for (int m = 0; m < weights.get(j)[k].length; m++) {
						sum = sum + salidas.get(j)[m] * weights.get(j)[k][m];
					}
					aux[k + 1] = function.eval(sum);

				}
				salidas.add(aux);
			}
			resp[i] = salidas.get(salidas.size() - 1)[1];
		}
		return resp;
	}

	private void datos(String fileName) throws FileNotFoundException {
		Scanner s = new Scanner(new File("./data/" + fileName));
		int cant = 441;
		dataInput = new double[cant][3];
		expectedOutput = new double[cant];
		for (int i = 0; i < cant; i++) {
			dataInput[i][0] = -1;
			dataInput[i][1] = s.nextDouble();
			dataInput[i][2] = s.nextDouble();
			expectedOutput[i] = s.nextDouble();
		}

		s.close();
	}

	private void cambiarRango() {
		double minV = expectedOutput[0];
		double maxV = expectedOutput[0];
		for (int i = 0; i < expectedOutput.length; i++) {
			if (expectedOutput[i] < minV) {
				minV = expectedOutput[i];
			} else if (expectedOutput[i] > maxV) {
				maxV = expectedOutput[i];
			}
		}

		for (int i = 0; i < expectedOutput.length; i++) {
			expectedOutput[i] = (expectedOutput[i] - (maxV + minV) / 2)
					* ((maxR - minR) / (maxV - minV)) + ((maxR + minR) / 2);
		}

	}

	public List<Double> backPropagation(List<Double> pesos, int epocas) {
		fromList(pesos);
		ArrayList<double[]> d;
		ArrayList<double[]> salidas;
		double[] o = new double[dataInput.length];
		for (int ep = 0; ep < epocas; ep++) {
			for (int i = 0; i < dataInput.length; i++) {
				d = new ArrayList<double[]>(layers.length - 1);
				for (int j = 0; j < layers.length - 1; j++) {
					d.add(null);
				}
				salidas = new ArrayList<double[]>(layers.length);
				salidas.add(dataInput[i]);
				for (int j = 0; j < weights.size(); j++) {
					double[] aux = new double[weights.get(j).length + 1];
					aux[0] = -1;
					for (int k = 0; k < weights.get(j).length; k++) {
						double sum = 0;
						for (int m = 0; m < weights.get(j)[k].length; m++) {
							sum = sum + salidas.get(j)[m]
									* weights.get(j)[k][m];
						}
						aux[k + 1] = function.eval(sum);

					}
					salidas.add(aux);
				}
				o[i] = salidas.get(salidas.size() - 1)[1];
				double[] ultimo = { salidas.get(salidas.size() - 1)[1] };
				salidas.add(salidas.size() - 1, ultimo);
				salidas.remove(salidas.size() - 1);
				// System.out.println(i+ " " + dataInput[i][0] + " " +
				// dataInput[i][1] + " " + dataInput[i][2] + " " +
				// salidas.get(salidas.size()-1)[0]);
				// System.out.println(i + " " + evaluate(dataInput)[i]);
				double sum = 0;
				for (int j = 0; j < weights.get(weights.size() - 1).length; j++) {
					for (int k = 0; k < weights.get(weights.size() - 1)[j].length; k++) {
						sum = sum + weights.get(weights.size() - 1)[j][k]
								* salidas.get(salidas.size() - 2)[k];
					}
				}
				double[] aux = { function_d.eval(sum)
						* (expectedOutput[i] - o[i]) };
				d.add(layers.length - 2, aux);
				d.remove(layers.length - 1);
				// System.out.println(i+ " " + aux[0]);
				int capa = layers.length - 3;

				while (capa >= 0) {
					double[] b = new double[weights.get(capa + 1)[0].length];
					for (int k = 0; k < weights.get(capa + 1)[0].length; k++) {
						sum = 0;
						for (int j = 0; j < weights.get(capa + 1).length; j++) {
							sum = sum + d.get(capa + 1)[j]
									* weights.get(capa + 1)[j][k];
						}
						b[k] = sum;
					}
					double[] a = new double[weights.get(capa).length];
					for (int j = 0; j < weights.get(capa).length; j++) {
						sum = 0;
						for (int k = 0; k < weights.get(capa)[j].length; k++) {
							sum = sum + weights.get(capa)[j][k]
									* salidas.get(capa)[k];
						}
						a[j] = function_d.eval(sum);
					}
					double[] dAux = new double[a.length];
					for (int j = 0; j < a.length; j++) {
						dAux[j] = a[j] * b[j + 1];
					}
					d.add(capa, dAux);
					d.remove(capa + 1);
					capa = capa - 1;

				}
				// System.out.println(i + " " + d.get(0)[0] + " " +
				// d.get(0)[1]);
				// System.out.println("deltas : " + d);
				capa = 0;
				double[][] dw;
				while (capa < layers.length - 1) {
					dw = new double[d.get(capa).length][salidas.get(capa).length];
					for (int j = 0; j < d.get(capa).length; j++) {
						for (int k = 0; k < salidas.get(capa).length; k++) {
							dw[j][k] = eta * salidas.get(capa)[k]
									* d.get(capa)[j];
						}
					}
					for (int j = 0; j < d.get(capa).length; j++) {
						for (int k = 0; k < salidas.get(capa).length; k++) {
							weights.get(capa)[j][k] = weights.get(capa)[j][k]
									+ dw[j][k];
						}
					}
					capa = capa + 1;
				}

			}
		}
		return toList();
	}

}
